# field validation model
import json
import logging
import re
import traceback
from functools import wraps

from air_web.web_flask.common import constants
from air_web.web_flask.common.tools import get_default_result

log = logging.getLogger("gate_server")


# field type
T_INT = 1
T_FLOAT = 2
T_STR = 4
T_LIST = 5
T_LIST_CHINESE = 6
T_LIST_NO = 7
T_MAIL = 8
T_IP = 12
T_MOBILE = 16
T_NO = 17
T_USERNAME = 18
T_DATE = 20
T_DATETIME = 24
T_TIMESTAMP = 28
T_CHINESE = 32
T_PWD = 36


class CustomizeMatch:
    """Customize fields' matching function"""

    @staticmethod
    def forbid_include_in_fields(val, special_field):
        """Forbid some special words included in special fields"""
        if not all(val.values()):
            return True

        special_field = val[special_field].lower()
        forbid_words = [
            "admin",
            "password",
            "root",
            "r00t",
            "toor",
            "t00r",
            "weblogic",
            "zshield",
        ]
        for word in forbid_words:
            if word in special_field:
                return False
        return True

    @staticmethod
    def field_not_in_field(val, former_field, latter_field):
        """the match method of combination fields of one field and another field"""
        if not all(val.values()):
            return True

        former_field = val[former_field]
        latter_field = val[latter_field]

        if former_field == "name" and not former_field:
            return True

        if former_field.lower() in latter_field.lower():
            return False
        return True


# validation rule
TYPE_MAP = {
    T_MAIL: re.compile(r"^[a-zA-Z0-9_\-\'.]+@[a-zA-Z0-9_]+(\.[a-z]+){1,2}$"),
    T_NO: re.compile(r"^[a-zA-Z0-9-_]{1,40}$"),
    T_USERNAME: re.compile("^[a-zA-Z0-9_\u4e00-\u9fa5]+$"),
    T_IP: re.compile(r"^([0-9]{1,3}\.){1,3}[0-9{1,3}]$"),
    T_MOBILE: re.compile(r"^1[3578][0-9]{9}$"),
    T_DATE: re.compile(r"^2[0-9]{3}([-/])[0-9]{1,2}([-/])[0-9]{1,2}$"),
    T_DATETIME: re.compile(
        r"^2[0-9]{3}([-/])[0-9]{1,2}([-/])[0-9]{1,2} [0-9]{2}:[0-9]{2}:[0-9]{2}$"
    ),
    T_TIMESTAMP: re.compile(r"^[0-9]{1,10}$"),
    T_CHINESE: re.compile(r"^[\u4e00-\u9fa5_a-zA-Z0-9#/.=\-]+$"),
    T_PWD: re.compile(
        r"^(?=.*[A-Za-z])(?=.*\d)(?=.*[$@$!%*#?&])[A-Za-z\d$@$!%*#?&]{8,24}$"
    ),
}


class Fields:
    """fields needed to be verify"""

    def __init__(self, name, val_type=T_STR, must=False, default=None):
        self.name = name
        self.value = None
        # the type which we expected
        self.type = val_type
        # whether the field is necessary
        self.must = must
        self.op = "="
        self.default = default
        self.match = ""

        if val_type >= T_MAIL:
            self.match = TYPE_MAP[val_type]


class CombineFields:
    """combination fields"""

    def __init__(self, fields, val_type):
        """
        receive a list consisted of Fields objects and a type the combination fields should be
        """
        # for example: org_no_date_verify
        self.name = ""
        self.fields = fields
        self.type = val_type
        # the customized function to process or verify combination fields
        self.match = None
        # specify invalid response for the combination field
        self.response = None

        if val_type >= T_MAIL:
            self.match = TYPE_MAP[val_type]


class ValidationError(Exception):
    """验证失败，抛出错误"""

    pass


class Validator:
    """verify request fields"""

    def __init__(self, fields):
        # the fields we want to verify
        self._fields = []
        # the fields have been verified
        self.data = {}
        # the fields are invalid
        self.invalid_fields = []
        # the combination fields are invalid
        self.combination_fields = {}

        combination_fields = []
        for f in fields:
            if isinstance(f, CombineFields):
                for field in f.fields:
                    self._fields.append(field)
                combination_fields.append(f)
            else:
                self._fields.append(f)
        # combination fields is at the end of list
        self._fields.extend(combination_fields)

    def _check_item(self, field, val):
        """check input data value is valid for the field"""
        if field.type == T_INT:
            return int(val)

        elif field.type == T_FLOAT:
            return float(val)

        elif field.type == T_LIST:
            if isinstance(val, str):
                val = json.loads(val)
            if not isinstance(val, list):
                raise ValidationError
            return val

        elif field.type == T_LIST_CHINESE:
            if isinstance(val, str):
                val = json.loads(val)
            if not isinstance(val, list):
                raise ValidationError
            for v in val:
                if v and not TYPE_MAP[T_CHINESE].match(v):
                    raise ValidationError
            return val

        elif field.type == T_LIST_NO:
            if isinstance(val, str):
                val = json.loads(val)
            if not isinstance(val, list):
                raise ValidationError
            for v in val:
                if v and not TYPE_MAP[T_NO].match(v):
                    raise ValidationError
            return val

        elif field.type == T_STR:
            if field.match:
                if not field.match.match(val):
                    raise ValidationError(field.name)
                return val
            return val

        elif field.type >= 50:
            if not field.match(val):
                # invalid combination field and specific response
                self.combination_fields[field.name] = {
                    "is_invalid": True,
                    "response": field.response(val),
                }
            else:
                # valid combination field
                self.combination_fields[field.name] = {
                    "is_invalid": False,
                    "response": None,
                }

        else:
            if not field.match.match(val):
                raise ValidationError(field.name)
            return val

    def verify(self, request_data):
        """字段验证入口"""
        # check fields and transfer type
        for f in self._fields:
            try:
                if not isinstance(f, CombineFields):
                    val = request_data.get(f.name)

                    if (not val and val != 0) or val == "[]":
                        if f.must:
                            # if the field is necessary --->invalid
                            self.invalid_fields.append(f.name)
                        elif f.default is not None:
                            f.value = f.default
                            # save valid data
                            self.data[f.name] = f.value
                        else:
                            self.data[f.name] = None
                        continue

                # get each combination fields' values from self.data
                else:
                    val = {}
                    combination_name = ""
                    for field in f.fields:
                        combination_name += field.name + "_"
                        val[field.name] = self.data.get(field.name)
                    f.name = combination_name + "verify"

                # check value
                f.value = self._check_item(f, val)
                # save valid data
                self.data[f.name] = f.value

            except ValidationError:
                traceback.print_exc()
                self.invalid_fields.append(f.name)
            except ValueError:
                traceback.print_exc()
                self.invalid_fields.append(f.name)
            except TypeError:
                traceback.print_exc()
                self.invalid_fields.append(f.name)
            except:
                log.error(traceback.format_exc())

        return 1


def with_validator(request, fields):
    """The view func with this decorator will verify request data automatically"""

    def validation_func(func):
        @wraps(func)
        def _(*args, **kwargs):
            resp = get_default_result()

            if request.method == "GET":
                request_data = request.values.to_dict()
            elif request.method == "POST":
                if not request.json:
                    request_data = request.form
                else:
                    request_data = request.json

            else:
                resp["status"] = constants.Status.REQUEST_METHOD_NOT_ALLOW
                resp["msg"] = constants.StatusMsg.REQUEST_METHOD_NOT_ALLOW
                return json.dumps(resp)

            if not request_data:
                request_data = {}

            validator = Validator(fields)
            validator.verify(request_data)

            # save valid data in the request object
            request.validation_data = validator.data
            request.combination_fields = validator.combination_fields

            if validator.invalid_fields:
                validator.invalid_fields = list(set(validator.invalid_fields))
                resp["status"] = constants.Status.PARAM_ERROR
                resp["msg"] = constants.StatusMsg.PARAM_ERROR + "{}".format(
                    validator.invalid_fields
                )
                return json.dumps(resp)

            if validator.combination_fields:
                res_list = []
                for field_name, res in validator.combination_fields.items():
                    if res["is_invalid"]:
                        res_list.append(res["response"])
                    else:
                        continue
                if res_list:
                    resp["status"] = constants.Status.PARAM_ERROR
                    resp["msg"] = "提示：{}".format(res_list)
                    return json.dumps(resp)

            return func(*args, **kwargs)

        return _

    return validation_func


def test1():
    fields = [
        Fields("id", T_INT, True),
        Fields("name", T_STR, default="nihao"),
        Fields("date", T_DATE, default="2020-10-10"),
        Fields("org_no", T_NO, default="123"),
        Fields("mail", T_MAIL, True),
    ]

    input_data = {
        "id": "1234",
        "name": "",
        "date": "",
        "org_no": "1244323",
        "page_no": "123444",
        "mail": "will'yang@qq.com",
    }

    validator = Validator(fields)
    validator.verify(input_data)
    print(validator.invalid_fields)
    print(validator.data)
    print(validator.combination_fields)


if __name__ == "__main__":
    test1()
